package com.nulldev.util.scripting.ScriptAPI.baseJS.legacy.mods.emath;

import java.util.Map;

import com.nulldev.util.data.FastMath.FastMath;
import com.nulldev.util.internal.backport.concurrency9.Maps;

@SuppressWarnings("static-access")
public class baseJSMaths {

	private static final com.nulldev.util.data.FastMath.Math Math = FastMath.get();

	public @FunctionalInterface static interface Expression {
		double eval();
	}

	private static class baseJSMathParser {
		private final String str;
		private final Map<String, Double> variables;

		public baseJSMathParser(final String str) {
			this.str = str;
			this.variables = Maps.of();
		}

		public baseJSMathParser(final String str, final Map<String, Double> variables) {
			this.str = str;
			this.variables = variables;
		}

		int pos = -1, ch;

		void nextChar() {
			ch = (++pos < str.length()) ? str.charAt(pos) : -1;
		}

		boolean eat(int charToEat) {
			while (ch == ' ')
				nextChar();
			if (ch == charToEat) {
				nextChar();
				return true;
			}
			return false;
		}

		Expression parse() {
			nextChar();
			Expression x = parseExpression();
			if (pos < str.length())
				throw new RuntimeException("parse::Unexpected: " + (char) ch);
			return x;
		}

		// Grammar:
		// expression = term | expression `+` term | expression `-` term
		// term = factor | term `*` factor | term `/` factor
		// factor = `+` factor | `-` factor | `(` expression `)`
		// | number | functionName factor | factor `^` factor

		Expression parseExpression() {
			Expression x = parseTerm();
			for (;;) {
				if (eat('+')) { // addition
					Expression a = x, b = parseTerm();
					x = (() -> a.eval() + b.eval());
				} else if (eat('-')) { // subtraction
					Expression a = x, b = parseTerm();
					x = (() -> a.eval() - b.eval());
				} else if (eat('%')) {
					Expression a = x, b = parseTerm();
					x = (() -> a.eval() % b.eval());
				} else if (eat('*')) {
					Expression a = x, b = parseTerm();
					x = (() -> a.eval() * b.eval());
				} else {
					return x;
				}
			}
		}

		Expression parseTerm() {
			double x = parseFactor().eval();
			for (;;) {
				if (eat('*'))
					x *= parseFactor().eval(); // multiplication
				else if (eat('/'))
					x /= parseFactor().eval(); // division
				else {
					final double _x = x;
					return (() -> _x);
				}
			}
		}

		Expression parseFactor() {
			if (eat('+'))
				return parseFactor(); // unary plus
			if (eat('-'))
				return (() -> -parseFactor().eval()); // unary minus

			double x = 0.0d;
			int startPos = this.pos;
			if (eat('(')) { // parentheses
				x = parseExpression().eval();
				eat(')');
			} else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers
				while ((ch >= '0' && ch <= '9') || ch == '.')
					nextChar();
				x = Double.parseDouble(str.substring(startPos, this.pos));
			} else if (ch >= 'a' && ch <= 'z') { // functions
				while (ch >= 'a' && ch <= 'z')
					nextChar();
				String func = str.substring(startPos, this.pos);
				x = parseFactor().eval();
				if (func.equals("sqrt"))
					x = Math.sqrt(x);
				else if (this.variables.containsKey(func))
					x = this.variables.get(func);
				else if (func.equals("sin"))
					x = Math.sin(Math.toRadians(x));
				else if (func.equals("cos"))
					x = Math.cos(Math.toRadians(x));
				else if (func.equals("tan"))
					x = Math.tan(Math.toRadians(x));
				else
					throw new RuntimeException("Unknown function: " + func);
			} else if (eat('%')) {
				x = parseExpression().eval();
			} else if (eat('*')) {
				x = parseExpression().eval();
			} else {
				throw new RuntimeException("parseFactor::Unexpected: " + (char) ch);
			}

			if (eat('^'))
				x = Math.pow(x, parseFactor().eval()); // exponentiation

			final double _x = x;
			return (() -> _x);
		}
	}

	public static Expression eval(final String str) {
		return new baseJSMathParser(str).parse();
	}

	public static Expression eval(final String str, final Map<String, Double> variables) {
		return new baseJSMathParser(str, variables).parse();
	}
}
